前端當然要從 HTML 出發,今天就讓我們來聊聊 script tag 加上 async / defer 的功能及差異。
我們知道,瀏覽器解析 HTML 是一行一行依序向下讀取,在傳統的寫法中,當瀏覽器讀到 <script>
時,便會 暫停解析 DOM,立刻開始下載 <script>
的資源,並在下載完成後立刻執行。由於這樣的特性,便可能造成在 DOM 樹建構不完全時就執行 JavaScript,其中需要操作 DOM 的程式可能就因此無法正確運作,許多衍伸的問題也就因此產生;或著是在等待 <script>
資源下載、執行的過程,使用者便會卡在白畫面,並產生覺得網站太慢、不好用之類的感受。
而那時的解決方法也很簡單直白,開發者需要把 <script>
的位置都放到 <body>
的最後一行,來避免在 DOM 樹未建構完就開始執行程式的問題,但在更複雜的網站中,HTML、JavaScript 的檔案都來越大,下載、執行時間也越來越長,需要等到整個 DOM 樹都載入完成才開始下載 <script>
內的資源,從網站讀取完成到可操作之間便會有明顯的延遲感。
這樣的問題該怎麼解決呢?
本系列文已經重新編校彙整編輯成冊,並正式出版囉!
《前端三十:從 HTML 到瀏覽器渲染的前端開發者必備心法》好評販售中!
喜歡我文章內容的讀者們,歡迎您 前往購買 支持!
從 HTML4 開始,<script>
便多了 defer
屬性,HTML5 則多了 async
,兩者皆是用來幫助開發者控制 <script>
內資源的載入及執行順序,以及避免 DOM 的解析被資源下載卡住。
defer
意旨為 延遲(Deferred),在 HTML4.01 的規格 中定義了:
When set, this boolean attribute provides a hint to the user agent that the script is not going to generate any document content (e.g., no "document.write" in javascript) and thus, the user agent can continue parsing and rendering.
也就是說,加上 defer
屬性後,瀏覽器會繼續解析、渲染畫面,而不會因為需要載入 <script>
內的資源而卡在那邊等;實際上的執行時間,會在 DOMContentLoaded
執行之前,由上到下的依照擺放順序觸發。
聽起來很方便對吧?但要提醒各位讀者,雖然 W3C 規格上說 defer
屬性會是一個布林值,但 IE9 以前的版本是自定義的特規,即使寫成 <script defer="false">
仍然會有 defer
的效果,使用時要特別注意喔!
async
即為 非同步(Asynchronous),在 HTML5 的規格 中提到:
... If the async attribute is present, then the script will be executed asynchronously, as soon as it is available. ...
在 <script>
加上 async
屬性後,與 defer
相同的是會在背景執行下載,但不同的是當下載完成會馬上暫停 DOM 解析(若尚未解析完的話),並開始執行 JavaScript。也因為下載完成後會立即執行,加上 async
屬性後,就無法保證執行順序了。
這個屬性在標準中,同時也支援透過 JavaScript 動態塞入 <script>
的情境。例如:
const script = document.createElement('script')
script.src = "/something/awesome.js"
document.body.append(script)
動態建立的 <script>
,預設就會是非同步載入;但可以透過屬性設定將它關閉:
script.async = false
在主流的現代瀏覽器中,<script>
的屬性可以加上 type="module"
。這時,瀏覽器會將此檔案認為是一個 JavaScript 模組,其中的解析規則、執行環境會略有不同;這時候的 <script>
預設行為會像是 defer
一樣,背景下載,且等待 DOM 解析、渲染完成後才執行,也因此 defer
屬性無法在 type="module"
產生作用。但同樣可以透過 async
屬性讓它在下載完成後即刻執行。
前述的介紹及圖片解說,相信應該對這兩個屬性的特色已經不陌生了,那麼該如何正確地使用呢?
defer
由於背景載入、不打斷渲染及確保執行順序的特色,基本上沒特別需求的話,<script>
都設定一下就對了;當然,<script>
本身的擺放順序還是要稍微留心注意。async
比較特別,因為下載後會立刻執行,且不保證順序,一般常見的應用是設定在完全獨立的小模組,例如 GA、背景 Log、頁面廣告等等,在避免造成使用者體驗變差的同時,盡量提早開始產生效果。
當然,現代前端開發大都透過 Webpack 等打包工具協助處理,應該也鮮少有自己設定這些屬性的機會;開發者可以透過 script-ext-html-webpack-plugin 等套件的協助,將切分好的 Chunk 設定個別需要的 <script>
屬性。
async
及 defer
是專屬於 <script>
的屬性,而網頁中的其他資源,我們可以透過 <link>
的 preload
、prefetch
屬性,來幫我們 延遲載入 未來才需要用到的資源,詳細的可以參考 Summer 大大寫的 Preload vs Prefetch,筆者就不在這邊贅述了。
以上就是今天的 <script>
的 async
、defer
屬性介紹,雖然這些屬性的設定大都已經包在現代框架的打包流程中了,但我認為唯有紮實的認識這些網頁最基礎的規格,才能夠明白自己交出去的 Code 最後會產生什麼效果喔!希望這篇文章可以幫助大家認識這兩個重要的屬性,若有任何問題或不清楚的地方,也都歡迎大家回應指教!
筆者
Gary
半路出家網站工程師;半生熟的前端加上一點點的後端。
喜歡音樂,喜歡學習、分享,也喜歡當個遊戲宅。相信一切安排都是最好的路。